package malwarescan

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/pkg/errors"
)

// ScanResult : Returned by the scan endpoint of the malwarescan api of SAP CP
type ScanResult struct {
	MalwareDetected          bool   `json:"malwareDetected"`
	EncryptedContentDetected bool   `json:"encryptedContentDetected"`
	ScanSize                 int    `json:"scanSize"`
	Finding                  string `json:"finding,omitempty"`
	MimeType                 string `json:"mimeType"`
	SHA256                   string `json:"SHA256"`
}

// Info : Returned by the info endpoint of the malwarescan api of SAP CP
type Info struct {
	MaxScanSize        int
	SignatureTimestamp string
	EngineVersion      string
}

// ScanError : Returned by the malwarescan api of SAP CP in case of an error
type ScanError struct {
	Message string
}

// Client : Interface for the malwarescan api provided by SAP CP (see https://api.sap.com/api/MalwareScanAPI/overview)
type Client interface {
	Scan(candidate io.Reader) (*ScanResult, error)
	Info() (*Info, error)
}

// ClientImpl : Client implementation of the malwarescan api provided by SAP CP (see https://api.sap.com/api/MalwareScanAPI/overview)
type ClientImpl struct {
	HTTPClient piperhttp.Sender
	Host       string
}

// Scan : Triggers a malwarescan in SAP CP for the given content.
func (c *ClientImpl) Scan(candidate io.Reader) (*ScanResult, error) {
	var scanResult ScanResult

	headers := http.Header{}
	headers.Add("Content-Type", "application/octet-stream")

	err := c.sendAPIRequest("POST", "/scan", candidate, headers, &scanResult)

	if err != nil {
		return nil, err
	}

	return &scanResult, nil

}

// Info : Returns some information about the scanengine used by the malwarescan service.
func (c *ClientImpl) Info() (*Info, error) {
	var info Info

	err := c.sendAPIRequest("GET", "/info", nil, nil, &info)

	if err != nil {
		return nil, err
	}

	return &info, nil
}

func (c *ClientImpl) sendAPIRequest(method, endpoint string, body io.Reader, header http.Header, obj interface{}) error {
	// piper http utils mashall some http response codes into errors. We wan't to check the status code
	// ourselves hence we wait with returning that error (maybe also related to errors others than http status codes)

	// sendRequest results in any combination of nil and non-nil response and error.
	// a response body could even be already closed.
	response, err := c.HTTPClient.SendRequest(method, c.Host+endpoint, body, header, nil)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("Failed to send request to MalwareService."))
	}

	if response.StatusCode != 200 {
		var scanError ScanError

		err = c.unmarshalResponse(response, &scanError)

		if err != nil {
			return fmt.Errorf("MalwareService returned with status code %d, no further information available", response.StatusCode)
		}

		return fmt.Errorf("MalwareService returned with status code %d: %s", response.StatusCode, scanError.Message)
	}

	return c.unmarshalResponse(response, obj)
}

func (c *ClientImpl) readBody(response *http.Response) ([]byte, error) {
	if response != nil && response.Body != nil {
		defer response.Body.Close()
		return io.ReadAll(response.Body)
	}

	return nil, fmt.Errorf("No response body available")
}

func (c *ClientImpl) unmarshalResponse(response *http.Response, obj interface{}) error {
	body, err := c.readBody(response)

	if err != nil {
		return err
	}

	err = json.Unmarshal(body, obj)

	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("Unmarshalling of response body failed. Body: '%s'", body))
	}

	return err
}
